package com.sprouting.personal.annotations.aop;

import cn.hutool.core.util.StrUtil;
import com.sprouting.personal.annotations.ApiIdempotent;
import com.sprouting.personal.emums.ResultEnum;
import com.sprouting.personal.model.common.ResultJson;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 对冥等注解实现AOP切面
 * 注意，冥等验证只对有权限的才能使用，没有token的不适合使用本注解
 *
 * @author ：LX（长沙麓谷）
 * @date 创建时间： 2022/7/27 0:22
 */
@Aspect
@Slf4j
@Component
public class IdempotentAop {

    /**
     * token名称，防止token取其他的名字
     */
    @Value("${sa-token.token-name:token}")
    private String tokenName;

    /**
     * 线程安全的map，只适用于单机系统，这样也不用去考虑资源释放问题，多机版本的则不在这里使用
     */
    private static ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();


    /**
     * 切入点，拦截 controller
     */
    @Pointcut(value = "execution(* com..controller.*.*(..))")
    public void controllerPointCut() {

    }

    /**
     * 必须 controller 以及 注解才会拦截
     */
    @Pointcut("@annotation(com.sprouting.personal.annotations.ApiIdempotent) && controllerPointCut()")
    public void IdempotentAspect(){

    }

    /**
     * 环绕通知
     * @param proceedingJoinPoint 切入点
     * @return
     */
    @Around("IdempotentAspect()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 1、--- 获取request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        if (servletRequestAttributes != null){
            HttpServletRequest request = servletRequestAttributes.getRequest();
            // uri 只有后面的路径，url包含完整的请求路径。
            String uri = request.getRequestURI();
            // 加方法类型，用于兼容 result 风格
            String method = request.getMethod();
            // 获取token
            String token = request.getHeader(tokenName);
            if (StrUtil.isBlank(token)){
                // 不予执行
                log.warn("[冥等性] 未拦截到token信息，请求 {} - {} 未进行冥等性验证，直接放行!", uri, method);
                return proceedingJoinPoint.proceed();
            }

            // 2、--- 获取过期时间
            // 单机已经没有必要获取过期时间，直接在内存中保存key的状态
            // int lock = getAnnotation(proceedingJoinPoint);

            // 2.1 判断释放在加锁中，否则加锁
            String key = token + method + uri;
            // 当key不存在的时候则返回null，当key存在的时候，则返回旧的值，比如之前存入了 2，这个时候返回的是2，不会返回现在输入的1
            String value = concurrentHashMap.putIfAbsent(key, "1");
            if (StrUtil.isNotBlank(value)){
                log.info("[冥等性] 拦截到接口冥等不通过，请求 {} - {} 被打回", method, uri);
                return new ResultJson<String>(ResultEnum.SERVICE_UNAVAILABLE.getCode(), ResultEnum.ERROR.getStatus(), ResultEnum.SERVICE_UNAVAILABLE.getMsg(), null);
            }

            try {
                // 3、--- 调用业务代码
                Object proceed = proceedingJoinPoint.proceed();

                // 4、--- 释放锁
                // 通过再次设置这个值，并将过期时间设置为1毫秒，实现释放锁的功能
                concurrentHashMap.remove(key);

                // 一定要返回，否则返回数据丢失
                return proceed;

            } catch (Throwable throwable) {
                // 5、--- 当调用发生异常，释放锁
                concurrentHashMap.remove(key);
                // 5.1 必须再次抛出异常，否则统一异常处理不会接受到异常信息，影响到业务逻辑
                throw throwable;
            }

        } else {
            log.warn("[冥等性] 请求接口非web接口，未获取到web信息，不进行冥等性验证");
            return proceedingJoinPoint.proceed();
        }
    }

    /**
     * 获取注解返回过期时间
     * @param proceedingJoinPoint 连接点
     * @return
     */
    private int getAnnotation(ProceedingJoinPoint proceedingJoinPoint){
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        ApiIdempotent annotation = signatureMethod.getAnnotation(ApiIdempotent.class);

        // int lock = annotation.lock();
        // if (lock <= 0){
        //     lock = 60;
        // }

        return 60;
    }
}
